
import java.util.ArrayList;
import java.util.HashMap;

/**
 *
 * @author david
 */
public class AnalizadorSintactico
{
    private Token pt;
    private Token.Tipo ptt;
    private AnalizadorLexico al;
    private TablaSimbolos ts;
    private boolean mainFlag;
    // Almacenamiento temporal de atributos y métodos de una clase
    private ArrayList<String> bufferAtributos;
    private HashMap<String, Simbolo> atributosSimbolos;
    private ArrayList<String> bufferMetodos;
    private HashMap<String, Simbolo> metodosSimbolos;
    private ArrayList<String> bufferLocales;
    private HashMap<String, Simbolo> localesSimbolos;
    // Tokens esperados para cada no terminal
    private final String listaS = "'class' final de fichero";
    private final String listaC = "'class'";
    private final String listaD = "'double' 'int' 'void' '}'";
    private final String listaE = "'double' 'int' 'void'";
    private final String listaV = "'double' 'int'";
    private final String listaM = "'void'";
    private final String listaMid = "'main' identificador";
    private final String listaDecl = "'double' 'int' '}' identificador";
    private final String listaCuerpo = "'}' identificador";
    private final String listaInstr = "identificador";
    private final String listaAsig = "'(' '='";
    private final String listaFactor = "entero identificador real";

    public AnalizadorSintactico()
    {
	pt = null;
	ptt = null;
	al = null;
	ts = new TablaSimbolos();
	mainFlag = false;
	bufferAtributos = new ArrayList<String>();
	atributosSimbolos = new HashMap<String, Simbolo>();
	bufferMetodos = new ArrayList<String>();
	metodosSimbolos = new HashMap<String, Simbolo>();
	bufferLocales = new ArrayList<String>();
	localesSimbolos = new HashMap<String, Simbolo>();
    }

    public void analisisDescendenteRecursivo(String filename)
    {
	al = new AnalizadorLexico(filename);

	try
	{
	    pt = al.siguienteToken();

	    // Desechar los tokens comentario
	    while (pt != null && pt.getTipo() == Token.Tipo.COMENTARIO)
	    {
		pt = al.siguienteToken();
	    }

	    if (pt != null)
	    {
		ptt = pt.getTipo();

		String tocha = s();

		if (ptt == Token.Tipo.EOF)
		{
		    if (!mainFlag)
		    {
			mensajeErrorSemantico(9, null);
		    }

		    System.out.print(tocha);
		}
		else
		{
		    // Error
		    mensajeErrorSintactico("final de fichero");
		}
	    }
	}
	catch (PLException sex)
	{
	    System.err.println(sex.getMessage());
	}
    }

    private void mensajeErrorSintactico(String lista) throws PLException
    {
	String msgErr = "Error ";

	if (ptt != Token.Tipo.EOF)  // Error 3
	{
	    msgErr += "3 (" + pt.getLinea() + "," + pt.getColumna() + "): ";
	    msgErr += "encontrado '" + pt.getLexema() + "', ";
	    msgErr += "esperaba " + lista;
	}
	else			    // Error 4
	{
	    msgErr += "4: encontrado final de fichero, esperaba " + lista;
	}

	throw new PLException(msgErr);
    }

    private void mensajeErrorSemantico(int numero, Token t) throws PLException
    {
	String msgErr = "Error ";

	switch (numero)
	{
	    case 5:
		msgErr += "5 (" + t.getLinea() + "," + t.getColumna() + "): '";
		msgErr += t.getLexema() + "' ya existe en este ambito";
		break;
	    case 6:
		msgErr += "6 (" + t.getLinea() + "," + t.getColumna() + "): '";
		msgErr += t.getLexema() + "' no ha sido declarado";
		break;
	    case 7:
		msgErr += "7 (" + t.getLinea() + "," + t.getColumna() + "): '";
		msgErr += t.getLexema() + "' no es una variable";
		break;
	    case 8:
		msgErr += "8 (" + t.getLinea() + "," + t.getColumna() + "): '";
		msgErr += t.getLexema() + "' no es un metodo";
		break;
	    case 9:
		msgErr += "9: 'main' no esta definido";
		break;
	}

	throw new PLException(msgErr);
    }

    private String espacios()
    {
	String espacios = "";

	for (int i = 0; i < ts.getNivel(); i++)
	{
	    espacios += "    ";
	}

	return espacios;
    }

    private void emparejar(Token.Tipo tt) throws PLException
    {
	if (ptt == tt)
	{
	    pt = al.siguienteToken();

	    // Desechar los tokens comentario
	    while (pt != null && pt.getTipo() == Token.Tipo.COMENTARIO)
	    {
		pt = al.siguienteToken();
	    }

	    if (pt != null)
	    {
		ptt = pt.getTipo();
	    }
	    else
	    {
		ptt = null;
		// Error (el mensaje lo da el analizador léxico)
	    }
	}
	else
	{
	    // Error
	    mensajeErrorSintactico(tt.toString());
	}
    }

    private String s() throws PLException
    {
	if (ptt == Token.Tipo.CLASS)						// S -> C S
	{
	    return c() + s();
	}
	else if (ptt == Token.Tipo.EOF)						// S -> ε
	{
	    return "";
	}
	else
	{
	    // Error
	    mensajeErrorSintactico(listaS);
	    return null;
	}
    }

    private String c() throws PLException
    {
	if (ptt == Token.Tipo.CLASS)						// C -> class id llavei D llaved
	{
	    emparejar(Token.Tipo.CLASS);
	    Token id = pt;
	    emparejar(Token.Tipo.ID);

	    // Metemos el identificador en la tabla de símbolos
	    if (ts.insertar(id.getLexema(), Simbolo.Tipo.CLASE) == null)
	    {
		mensajeErrorSemantico(5, id);
	    }

	    emparejar(Token.Tipo.LLAVEI);
	    // Aquí empieza un nuevo ámbito
	    ts = new TablaSimbolos(ts);
	    // Limpiamos los buffers de atributos y métodos
	    bufferAtributos.clear();
	    atributosSimbolos.clear();
	    bufferMetodos.clear();
	    metodosSimbolos.clear();
	    d();
	    emparejar(Token.Tipo.LLAVED);
	    // Aquí se restaura el ámbito anterior
	    ts = ts.restaurar();
	    String cStr = espacios() + "class " + id.getLexema() + " {\n";

	    // Añadimos los atributos y metodos de los buffers que hayan sido referenciados
	    for (String s : bufferAtributos)
	    {
		if (atributosSimbolos.get(s).isReferenciado())
		{
		    cStr += s;
		}
	    }

	    for (String s : bufferMetodos)
	    {
		if (metodosSimbolos.get(s).isReferenciado() ||
		    metodosSimbolos.get(s).getNombre().equals("main"))
		{
		    cStr += s;
		}
	    }

	    return cStr + espacios() + "}\n";
	}
	else
	{
	    mensajeErrorSintactico(listaC);
	    return null;
	}
    }

    private void d() throws PLException
    {
	switch (ptt)
	{
	    case DOUBLE:							// D -> E D
	    case INT:
	    case VOID:
		e();
		d();
		break;
	    case LLAVED:								// D -> ε
		break;
	    default:
		// Error
		mensajeErrorSintactico(listaD);
	}
    }

    private void e() throws PLException
    {
	if (ptt == Token.Tipo.DOUBLE || ptt == Token.Tipo.INT)			// E -> V
	{
	    v(true);
	}
	else if (ptt == Token.Tipo.VOID)					// E -> M
	{
	    m();
	}
	else
	{
	    // Error
	    mensajeErrorSintactico(listaE);
	}
    }

    private void v(boolean atributo) throws PLException
    {
	if (ptt == Token.Tipo.DOUBLE || ptt == Token.Tipo.INT)						// V -> double id pyc
	{
	    String dato;

	    if (ptt == Token.Tipo.DOUBLE)
	    {
		emparejar(Token.Tipo.DOUBLE);
		dato = "double ";
	    }
	    else
	    {
		emparejar(Token.Tipo.INT);
		dato = "int ";
	    }

	    Token id = pt;
	    emparejar(Token.Tipo.ID);
	    // Metemos el identificador en la tabla de símbolos
	    Simbolo s = ts.insertar(id.getLexema(), Simbolo.Tipo.VARIABLE);
	    if (s == null)
	    {
		mensajeErrorSemantico(5, id);
	    }

	    emparejar(Token.Tipo.PYC);
	    String vStr = espacios() + dato + id.getLexema() + ";\n";

	    if (atributo)
	    {
		bufferAtributos.add(vStr);
		atributosSimbolos.put(vStr, s);
	    }
	    else
	    {
		bufferLocales.add(vStr);
		localesSimbolos.put(vStr, s);
	    }
	}
	else
	{
	    // Error
	    mensajeErrorSintactico(listaV);
	}
    }

    private void m() throws PLException
    {
	if (ptt == Token.Tipo.VOID)						// M -> void Mid pari pard llavei Decl Cuerpo llaved
	{
	    emparejar(Token.Tipo.VOID);
	    String mid = mid();
	    emparejar(Token.Tipo.PARI);
	    emparejar(Token.Tipo.PARD);
	    emparejar(Token.Tipo.LLAVEI);
	    // Aquí empieza un nuevo ámbito
	    ts = new TablaSimbolos(ts);
	    // Limpiamos el buffer de variables locales
	    bufferLocales.clear();
	    localesSimbolos.clear();
	    decl();
	    String cuerpo = cuerpo();
	    emparejar(Token.Tipo.LLAVED);
	    // Aquí se restaura el ámbito anterior
	    ts = ts.restaurar();
	    String mStr = espacios() + "void " + mid + " () {\n";
	    
	    // Añadimos las variables locales del buffer que hayan sido referenciadas
	    for (String s : bufferLocales)
	    {
		if (localesSimbolos.get(s).isReferenciado())
		{
		    mStr += s;
		}
	    }
	    
	    mStr += cuerpo + espacios() + "}\n";
	    bufferMetodos.add(mStr);
	    metodosSimbolos.put(mStr, ts.buscar(mid));
	}
	else
	{
	    // Error
	    mensajeErrorSintactico(listaM);
	}
    }

    private String mid() throws PLException
    {
	if (ptt == Token.Tipo.ID)						// Mid -> id
	{
	    Token id = pt;
	    emparejar(Token.Tipo.ID);

	    // Metemos el identificador en la tabla de símbolos
	    if (ts.insertar(id.getLexema(), Simbolo.Tipo.METODO) == null)
	    {
		mensajeErrorSemantico(5, id);
	    }

	    return id.getLexema();
	}
	else if (ptt == Token.Tipo.MAIN)					// Mid -> main
	{
	    Token main = pt;
	    emparejar(Token.Tipo.MAIN);

	    // Metemos el identificador en la tabla de símbolos
	    if (ts.insertar("main", Simbolo.Tipo.METODO) == null)
	    {
		mensajeErrorSemantico(5, main);
	    }

	    mainFlag = true;
	    return "main";
	}
	else
	{
	    // Error
	    mensajeErrorSintactico(listaMid);
	    return null;
	}
    }

    private void decl() throws PLException
    {
	if (ptt == Token.Tipo.DOUBLE || ptt == Token.Tipo.INT)			// Decl -> V Decl
	{
	    v(false);
	    decl();
	}
	else if (ptt == Token.Tipo.ID || ptt == Token.Tipo.LLAVED)		// Decl -> ε
	{
	    
	}
	else
	{
	    // Error
	    mensajeErrorSintactico(listaDecl);
	}
    }

    private String cuerpo() throws PLException
    {
	if (ptt == Token.Tipo.ID)						// Cuerpo -> Instr Cuerpo
	{
	    return instr() + cuerpo();
	}
	else if (ptt == Token.Tipo.LLAVED)					// Cuerpo -> ε
	{
	    return "";
	}
	else
	{
	    // Error
	    mensajeErrorSintactico(listaCuerpo);
	    return null;
	}
    }

    public String instr() throws PLException
    {
	if (ptt == Token.Tipo.ID)						// Instr -> id Asig pyc
	{
	    Token id = pt;
	    emparejar(Token.Tipo.ID);
	    // Buscamos el simbolo id en la tabla de símbolos
	    Simbolo s = ts.buscar(id.getLexema());

	    if (s == null)
	    {
		mensajeErrorSemantico(6, id);
	    }

	    s.setReferenciado(true);
	    String asig = asig(id, s.getTipo());
	    emparejar(Token.Tipo.PYC);
	    return espacios() + id.getLexema() + " " + asig + ";\n";
	}
	else
	{
	    // Error
	    mensajeErrorSintactico(listaInstr);
	    return null;
	}
    }

    public String asig(Token id, Simbolo.Tipo tipo) throws PLException
    {
	if (ptt == Token.Tipo.ASIG)						// Asig -> asig Factor
	{
	    // El simbolo de la regla padre tiene que ser una variable
	    if (tipo != Simbolo.Tipo.VARIABLE)
	    {
		mensajeErrorSemantico(7, id);
	    }

	    emparejar(Token.Tipo.ASIG);
	    return "= " + factor();
	}
	else if (ptt == Token.Tipo.PARI)					// Asig -> pari pard
	{
	    // El simbolo de la regla padre tiene que ser una variable
	    if (tipo != Simbolo.Tipo.METODO)
	    {
		mensajeErrorSemantico(8, id);
	    }

	    emparejar(Token.Tipo.PARI);
	    emparejar(Token.Tipo.PARD);
	    return "()";
	}
	else
	{
	    // Error
	    mensajeErrorSintactico(listaAsig);
	    return null;
	}
    }

    public String factor() throws PLException
    {
	Token factor = pt;

	switch (ptt)
	{
	    case REAL:								// Factor -> real
		emparejar(Token.Tipo.REAL);
		break;
	    case ENTERO:							// Factor -> entero
		emparejar(Token.Tipo.ENTERO);
		break;
	    case ID:								// Factor -> id
		emparejar(Token.Tipo.ID);

		// Buscamos el simbolo factor en la tabla de símbolos
		Simbolo s = ts.buscar(factor.getLexema());

		if (s == null)
		{
		    mensajeErrorSemantico(6, factor);
		}
		else if (s.getTipo() != Simbolo.Tipo.VARIABLE)
		{
		    mensajeErrorSemantico(7, factor);
		}

		s.setReferenciado(true);
		break;
	    default:
		// Error
		mensajeErrorSintactico(listaFactor);
		break;
	}

	return factor.getLexema();
    }

}
